
//
// 120425 - AH - Created.
// 120430a- AH - toString().
// 120430b- AH - Boolean return methods to tell input type.
// 120503 - AH - validate(HttpServletRequest).
// 120507a- AH - size property.
// 120507b- AH - ftype property can now have value "file".
// 120514 - AH - (Aura) rows, cols properties and support for textarea in html
//               form compilation.
// 120521 - AH - (Garage & Huset) select HTML control.
// 120620 - AH - Copy constructor.
// 120907a- AH - saveApiHtml(File).
// 120907b- AH - Improved description of name and type.
// 130310 - AH - toXml methods.
// 130312a- AH - clazz property renamed to type and now include Integer as
//               valid value and primitive types int, boolean and long.
// 130312b- AH - Renamed from HttpRequestParameter.
// 130316 - AH - Now extends HttpProtocolDef, name and desc property code
//               removed from here.
//

package org.japril.servlet;

import java.io.CharArrayWriter;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 *   Represents a HTTP Request parameter with information needed to render 
 *   HTML form for end user to give.
 * </p>
 * @author Arne Halvorsen (AH)
 */
public final class HttpRequestParamDef extends HttpProtocolElement 
{ 
  private Class<?> m_type=String.class; // annotation.
  
  private boolean m_mandatory=true; // annotation.
  
  private String m_ftype="text"; // annotation.
  
  private String m_fvalue=null; // annotation.
  
  private List<String> m_options=null; // annotation. 
      
  private String m_info=null; // annotation.
  
  private int m_size=-1; // annotation.
  
  private int m_cols=-1; // annotation.
  
  private int m_rows=-1; // annotation.
  
  /**
   * <p>
   *   Copy constructor.
   * </p>
   * @param o Object to copy.
   */
  public HttpRequestParamDef(HttpRequestParamDef o)
  {
    super(o.name());
    
    desc(o.description());
    
    m_type=o.m_type;
    m_cols=o.m_cols;
    m_ftype=o.m_ftype;
    m_fvalue=o.m_fvalue;
    m_info=o.m_info;
    m_mandatory=o.m_mandatory;
    m_rows=o.m_rows;
    m_size=o.m_size;
    
    m_options=(o.m_options==null) ? null : new ArrayList<String>(o.m_options);
  }
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * @param name {@link #name()}.
   */
  public HttpRequestParamDef(String name)
  {
    super(name);
  
    m_type=String.class;
    m_mandatory=true;
  }
  
  /**
   * <p>
   *   Sets description as does
   *   {@link #description(String)} but this returns this of concrete type. 
   * </p>
   * @param desc Description to set.
   * @return {@code this}.
   */
  public HttpRequestParamDef desc(String desc)
  { 
    description(desc); 
    return this; 
  }
  
  /**
   * <p>
   *   Sets java type of parameter (default is {@code Class.String}).
   * </p>
   * @param clazz Class.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code clazz} is not 
   *         {@code String.class || Integer.class || Long.class || Boolean.class}.
   */
  public HttpRequestParamDef type(Class<?> clazz)
  {
    if (clazz==null)
    {
      throw new NullPointerException("clazz");
    }
    
    if (clazz==String.class  ||
        clazz==Integer.class ||
        clazz==Integer.TYPE  ||
        clazz==Long.class    ||
        clazz==Long.TYPE     ||
        clazz==Boolean.class ||
        clazz==Boolean.TYPE
       )
    {
      m_type=clazz;
      return this;
    }
    
    throw new IllegalArgumentException("clazz: "+clazz+" not supported");
  }
  
  /**
   * <p>
   *   Gets type of parameter.
   * </p>
   * @return Class of parameter.
   */
  public Class<?> type(){ return m_type; }
  
  /**
   * <p>
   *   Sets if parameter is mandatory (default) or not.
   * </p>
   * @param v {@code true} if is, {@code false} if is not.
   * @return {@code this}.
   */
  public HttpRequestParamDef mandatory(boolean v)
  { 
    m_mandatory=v; 
    return this; 
  }
  
  /**
   * <p>
   *   Gets if parameter is mandatory.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean mandatory(){ return m_mandatory; }
  
  /**
   * <p>
   *   Sets form type (default is {@code "text"}).
   * </p>
   * @param t Form type.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code t} is not 
   *         {@code "text" || "password" || "radio" || "checkbox" || "file" || "submit"}}.
   */
  public HttpRequestParamDef ftype(String t)
  {
    if (t==null)
    {
      throw new NullPointerException("t");
    }
    
    if (t.equals("text")     ||
        t.equals("textarea") ||
        t.equals("password") ||
        t.equals("radio")    ||
        t.equals("checkbox") ||
        t.equals("submit")   ||
        t.equals("file")     ||
        t.equals("select")
       )
    {
      m_ftype=t;
      return this;
    }
    
    throw new IllegalArgumentException("uknown/unsuported form type: '"+t+"'");
  }
  
  /**
   * <p>
   *   Gets form type.
   * </p>
   * @return Type.
   */
  public String ftype(){ return m_ftype; }
  
  /**
   * <p>
   *   Tells if is submit type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isSubmit(){ return m_ftype.equals("submit"); }
  
  /**
   * <p>
   *   Tells if is checkbox type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isCheckbox(){ return m_ftype.equals("checkbox"); }
  
  /**
   * <p>
   *   Tells if is radio type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isRadio(){ return m_ftype.equals("radio"); }
  
  /**
   * <p>
   *   Tells if is password type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isPassword(){ return m_ftype.equals("password"); }
  
  /**
   * <p>
   *   Tells if is text type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isText(){ return m_ftype.equals("text"); }
  
  /**
   * <p>
   *   Tells if is textarea type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isTextArea(){ return m_ftype.equals("textarea"); }
  
  /**
   * <p>
   *   Tells if is select type.
   * </p>
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean isSelect(){ return m_ftype.equals("select"); }
  
  /**
   * <p>
   *   Sets value to be selected in a combo box.
   * </p>
   * @param v Value to be selected.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code v} is the empty string.
   */
  public HttpRequestParamDef option(String v)
  {
    if (v==null)
    {
      throw new NullPointerException("v");
    }
    
    v=v.trim();
    if (v.length()==0)
    {
      throw new IllegalArgumentException("v is the empty");
    }
    
    if (m_options==null) m_options=new ArrayList<String>();
    if (!m_options.contains(v)) m_options.add(v);
    
    return this;
  }
  
  /**
   * <p>
   *   Gets values to be selected in a combo box.
   * </p>
   * @return Values.
   */
  public String[] option()
  { 
    return m_options==null ? E_STRING_A : 
           m_options.toArray(E_STRING_A);
  }
  
  /**
   * <p>
   *   Sets value as apply for radio and checkbox controls in form.
   * </p>
   * @param v Value to set or {@code null} if to clear.
   * @return {@code this}.
   */
  public HttpRequestParamDef fvalue(String v){ m_fvalue=v; return this; }
  
  /**
   * <p>
   *   Gets value as apply for radio and checkbox controls in form.
   * </p>
   * @return Value or {@code null} if does not have.
   */
  public String fvalue(){ return m_fvalue; }
  
  /**
   * <p>
   *   Sets information on parameter.
   * </p>
   * @param info Information or {@code null} if to clear.
   * @return {@code this}.
   */
  public HttpRequestParamDef info(String info){ m_info=info; return this; }
  
  /**
   * <p>
   *   Gets information on parameter.
   * </p>
   * @return Information or {@code null} if does not have information.
   */
  public String info(){ return m_info; }
  
  /**
   * <p>
   *   Sets size to be used in a form.
   * </p>
   * @param size Size.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code size<1}.
   */
  public HttpRequestParamDef size(int size)
  {
    if (size<1)
    {
      throw new IllegalArgumentException("size<1 : "+size);
    }
    
    m_size=size;
    return this;
  }
  
  /**
   * <p>
   *   Gets size value to be used in a form.
   * </p>
   * @return Size or {@code -1} if not sat.
   */
  public int size(){ return m_size; }
  
  /**
   * <p>
   *   Sets cols to be used in a form.
   * </p>
   * @param cols Columns.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code cols<1}. 
   */
  public HttpRequestParamDef cols(int cols)
  {
    if (cols<1)
    {
      throw new IllegalArgumentException("cols<1 : "+cols);
    }
    
    m_cols=cols;
    return this;
  }
  
  /**
   * <p>
   *   Gets cols value to be used in a form.
   * </p>
   * @return Cols or {@code -1} if not sat.
   */
  public int cols(){ return m_cols; }
  
  /**
   * <p>
   *   Sets rows to be used in a form.
   * </p>
   * @param rows Rows.
   * @return {@code this}.
   * @throws IllegalArgumentException If {@code rows<1}. 
   */
  public HttpRequestParamDef rows(int rows)
  {
    if (rows<1)
    {
      throw new IllegalArgumentException("rows<1 : "+rows);
    }
    
    m_rows=rows;
    return this;
  }
  
  /**
   * <p>
   *   Gets rows value to be used in a form.
   * </p>
   * @return Rows or {@code -1} if not sat.
   */
  public int rows(){ return m_rows; }
  
  /**
   * <p>
   *   Checks if given
   *   {@link HttpServletRequest} can be accepted given {@code this}
   *   parameter description.
   * </p>
   * @param req {@link HttpServletRequest}.
   * @return If not accepted an error message, if accepted returns {@code null}.
   */
  public String validate(HttpServletRequest req)
  {
    if (req==null)
    {
      throw new NullPointerException("req");
    }
    
    FetchParameterResult fpr=getParameter(req);
    return fpr.error() ? fpr.errorMsg() : null;
  }
  
  /**
   * <p>
   *   Gets parameter value from given 
   *   {@link HttpServletRequest} as described by {@code this}.
   * </p>
   * @param req {@link HttpServletRequest} to fetch parameter value from.
   * @return Fetch result.
   */
  public FetchParameterResult getParameter(HttpServletRequest req)
  {
    if (req==null)
    {
      throw new NullPointerException("req");
    }
    
    String name=name();
    
    String value=req.getParameter(name);
    value=(value==null) ? "" : value.trim();
    value=(value.length()==0) ? null : value;
    
    if (value==null)
    {
      if (m_mandatory)
      {
        return new FetchParameterResult(true, "Missing parameter: "+name);
      }
      return new FetchParameterResult();
    }
    else
    {
      if (m_type==String.class)
      {
        return new FetchParameterResult(value);
      }
      if (m_type==Long.class)
      {
        return getLongParam(value);
      }
      if (m_type==Boolean.class)
      {
        return getBooleanParam(value);
      }
      
      throw new Error();
    }
  }
  
  private FetchParameterResult getLongParam(String s)
  {
    try
    {
      return new FetchParameterResult(Long.parseLong(s));
    }
    catch (NumberFormatException nfx)
    {
      return new FetchParameterResult(true, "Can not parse parameter: "+name()+
        " (value: "+s+") as long");
    }
  }
  
  private FetchParameterResult getBooleanParam(String s)
  {
    if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("yes"))
    {
      return new FetchParameterResult(true);
    }
    else if (s.equalsIgnoreCase("false") || s.equalsIgnoreCase("no"))
    {
      return new FetchParameterResult(true);
    } 
    
    return new FetchParameterResult(true, "Can not parse parameter: "+name()+
      " (value: "+s+") as long");
  }
  
  /**
   * <p>
   *   Creates HTML for this parameter in a HTML for.
   * </p>
   * @return HTML.
   */
  public String compileApiHtml()
  { 
    StringBuilder sb=new StringBuilder();
    
    String desc=description();
    if (desc!=null) sb.append(desc);
    
    String name=name();
    
    if (isSelect())
    {
      sb.append("<select name='").append(name()).append("'>");
      
      for (String option : m_options)
      {
        sb.append(" <option value='").append(option).append("'>")
          .append(option).append("</option>");
      }
      
      sb.append("</select>");
    }
    else if (isTextArea())
    {
      sb.append("<br/><textarea name='").append(name).append("'");
      if (m_cols!=-1) sb.append(" cols='").append(m_cols).append("'");
      if (m_rows!=-1) sb.append(" rows='").append(m_rows).append("'");
      sb.append(">");
      sb.append("</textarea>");
    }
    else
    {
      sb.append("<input type='").append(m_ftype).append("' name='")
        .append(name).append("'");
    
      if (m_fvalue!=null) sb.append(" value='").append(m_fvalue).append("'");
    
      if (m_size!=-1) sb.append("  size='").append(m_size).append("'");
    
      sb.append(" />");
    
      if (m_fvalue!=null) sb.append(' ').append(m_fvalue);
    
      if (m_mandatory) sb.append(" (*)");
    
      if (m_info!=null) sb.append("  ").append(m_info);
    }
    
    sb.append("<br /><code>");
    
    sb.append("request parameter name: ").append(name).append("<br/>");
    sb.append("request parameter type: ").append(classToType(m_type))
      .append("<br/>");
    
    sb.append("</code>");
    
    return sb.toString();
  }
  
  private static final Set<Class<?>> INTEGERS=new HashSet<Class<?>>();
  private static final Set<Class<?>> NUMBERS=new HashSet<Class<?>>();
  static
  {
    INTEGERS.add(Short.class);
    INTEGERS.add(Integer.class);
    INTEGERS.add(Long.class);
    NUMBERS.add(Float.class);
    NUMBERS.add(Double.class);
  } 
  
  private static String classToType(Class<?> clazz)
  {
    StringBuilder sb=new StringBuilder();
    String sname=clazz.getSimpleName().toLowerCase();
    if (INTEGERS.contains(clazz))
    {
      sb.append("integer (").append(sname).append(")");
    }
    else if (NUMBERS.contains(clazz))
    {
      sb.append("number (").append(sname).append(")");
    }
    else
    {
      sb.append(sname);
    }
    return sb.toString();
  }
  
  /**
   * <p>
   *   Writes 
   *   {@link #compileApiHtml()} to a HTML file.
   * </p>
   * <p>
   *   Provided mainly for test purposes.
   * </p>
   * @param f File to write to. If {@code null} write to file 
   *          {@code ./api.html}.
   * @throws IOException If fails to open or write to file.
   */
  public void saveApiHtml(File f) throws IOException
  {
    f=(f==null) ? new File("./api.html") : f;
    PrintWriter pw=null;
    try
    {
      pw=new PrintWriter(f);
      pw.println("<html><body>");
      pw.println(compileApiHtml());
      pw.println("</body></html>");
      pw.flush();
    }
    finally
    {
      if (pw!=null) pw.close();
    }
  }
  
  /**
   * <p>
   *   Gets XML representation.
   * </p>
   * @return XML.
   */
  public String toXml(){ return toXml(true, ""); }
  
  /**
   * <p>
   *   Gets XML representation.
   * </p>
   * @param standalone If to get standalone XML.
   * @return XML.
   */
  public String toXml(boolean standalone){ return toXml(standalone, ""); }
  
  /**
   * <p>
   *   Gets XML representation.
   * </p>
   * @param standalone If to get standalone XML.
   * @param indent     Indent to start at.
   * @return XML.
   */
  public String toXml(boolean standalone, String indent)
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    StringBuilder sb=new StringBuilder();
    
    if (standalone)
    {
      sb.setLength(0);
      sb.append(indent).append("<?xml version='1.0'?>");
      pw.println(sb);
    }
    
    sb.setLength(0);
    sb.append(indent).append("<http-request-parameter name='").append(name())
      .append("' class='").append(m_type.getName()).append("' mandatory='")
      .append(m_mandatory).append("' ftype='").append(m_ftype).append("'");
    
    if (m_fvalue!=null) sb.append(" fvalue='").append(m_fvalue).append("'");
    
    if (m_size!=-1) sb.append("  size='").append(m_size).append("'");
    
    if (m_cols!=-1) sb.append("  cols='").append(m_cols).append("'");
    
    if (m_rows!=-1) sb.append("  rows='").append(m_rows).append("'");
    
    sb.append(">");
    pw.println(sb);
    
    String desc=description();
    if (desc!=null)
    {
      sb.setLength(0);
      sb.append(indent).append("  <desc>").append(desc).append("</desc>");
      pw.println(sb);
    }
    
    if (m_info!=null)
    {
      sb.setLength(0);
      sb.append(indent).append("  <par-info>").append(m_info)
        .append("</par-info>");
      pw.println(sb);
    }
    
    if (m_options!=null && !m_options.isEmpty())
    {
      sb.setLength(0);
      sb.append(indent).append("  <options>");
      pw.println(sb);
      
      for (String option : m_options)
      {
        sb.append(indent).append("    <option value='").append(option)
          .append("'/>");
      }
      
      sb.setLength(0);
      sb.append(indent).append("  </options>");
      pw.println(sb);
    }
    
    sb.setLength(0);
    sb.append(indent).append("</http-request-parameter>");
    pw.println(sb);
    
    pw.flush();
    return caw.toString();
  }
  
  @Override
  public String toString() 
  {   
    StringBuilder sb=new StringBuilder();
    
    sb.append("name=").append(name()).append(", class=")
      .append(m_type.getName()).append(", mandatory=").append(m_mandatory)
      .append(", ftype=").append(m_ftype);
    
    String desc=description();
    
    if (m_fvalue!=null) sb.append(", fvalue=").append(m_fvalue);
    if (desc!=null) sb.append(", desc=").append(desc);
    if (m_info!=null) sb.append(", m_info=").append(m_info);
    
    return sb.toString();
  }
  
  private final static String[] E_STRING_A=new String[0];
  
  public static void main(String[] args)
  {
    try
    {
      HttpRequestParamDef FILE_ID=
        new HttpRequestParamDef("file-id").desc("File Id").type(Long.class);
    
      FILE_ID.saveApiHtml(null);
      System.exit(0);
    }
    catch (IOException iox)
    {
      iox.printStackTrace();
      System.exit(-1);
    }
  }
  
}
